<!DOCTYPE html>
<html>
  <head>
    <title>
      Test Convolver Channel Outputs for Response with 1 channel
    </title>
    <script src="/resources/testharness.js"></script>
    <script src="/resources/testharnessreport.js"></script>
    <script src="/webaudio/resources/audit-util.js"></script>
    <script src="/webaudio/resources/audit.js"></script>
  </head>
  <body>
    <script id="layout-test-code">
      // Test various convolver configurations when the convolver response has
      // one channel (mono).

      // This is somewhat arbitrary.  It is the minimum value for which tests
      // pass with both FFmpeg and KISS FFT implementations for 256 points.
      // The value was similar for each implementation.
      const absoluteThreshold = Math.pow(2, -21);

      // Fairly arbitrary sample rate, except that we want the rate to be a
      // power of two so that 1/sampleRate is exactly respresentable as a
      // single-precision float.
      let sampleRate = 8192;

      // A fairly arbitrary number of frames, except the number of frames should
      // be more than a few render quanta.
      let renderFrames = 10 * 128;

      let audit = Audit.createTaskRunner();

      // Convolver response
      let response;

      audit.define(
          {
            label: 'initialize',
            description: 'Convolver response with one channel'
          },
          (task, should) => {
            // Convolver response
            should(
                () => {
                  response = new AudioBuffer(
                      {numberOfChannels: 1, length: 2, sampleRate: sampleRate});
                  response.getChannelData(0)[1] = 1;
                },
                'new AudioBuffer({numberOfChannels: 1, length: 2, sampleRate: ' +
                    sampleRate + '})')
                .notThrow();

            task.done();
          });

      audit.define(
          {label: '1-channel input', description: 'produces 1-channel output'},
          (task, should) => {
            // Create a 3-channel context:  channel 0 = convolver under test,
            // channel 1: test that convolver output is not stereo, channel 2:
            // expected output.  The context MUST be discrete so that the
            // channels don't get mixed in some unexpected way.
            let context = new OfflineAudioContext(3, renderFrames, sampleRate);
            context.destination.channelInterpretation = 'discrete';

            let src = new OscillatorNode(context);
            let conv = new ConvolverNode(
                context, {disableNormalization: true, buffer: response});

            // Splitter node to verify that the output of the convolver is mono.
            // channelInterpretation must be 'discrete' so we don't do any
            // mixing of the input to the node.
            let splitter = new ChannelSplitterNode(
                context,
                {numberOfOutputs: 2, channelInterpretation: 'discrete'});

            // Final merger to feed all of the individual channels into the
            // destination.
            let merger = new ChannelMergerNode(context, {numberOfInputs: 3});

            src.connect(conv).connect(splitter);
            splitter.connect(merger, 0, 0);
            splitter.connect(merger, 1, 1);

            // The convolver response is a 1-sample delay.  Use a delay node to
            // implement this.
            let delay =
                new DelayNode(context, {delayTime: 1 / context.sampleRate});
            src.connect(delay);
            delay.connect(merger, 0, 2);

            merger.connect(context.destination);

            src.start();

            context.startRendering()
                .then(audioBuffer => {
                  // Extract out the three channels
                  let actual = audioBuffer.getChannelData(0);
                  let c1 = audioBuffer.getChannelData(1);
                  let expected = audioBuffer.getChannelData(2);

                  // c1 is expected to be zero.
                  should(c1, '1: Channel 1').beConstantValueOf(0);

                  // The expected and actual results should be identical
                  should(actual, 'Convolver output')
                    .beCloseToArray(expected,
                                    {absoluteThreshold: absoluteThreshold});
                })
                .then(() => task.done());
          });

      audit.define(
          {label: '2-channel input', description: 'produces 2-channel output'},
          (task, should) => {
            downMixTest({numberOfInputs: 2, prefix: '2'}, should)
                .then(() => task.done());
          });

      audit.define(
          {
            label: '3-channel input',
            description: '3->2 downmix producing 2-channel output'
          },
          (task, should) => {
            downMixTest({numberOfInputs: 3, prefix: '3'}, should)
                .then(() => task.done());
          });

      audit.define(
          {
            label: '4-channel input',
            description: '4->2 downmix producing 2-channel output'
          },
          (task, should) => {
            downMixTest({numberOfInputs: 4, prefix: '4'}, should)
                .then(() => task.done());
          });

      audit.define(
          {
            label: '5.1-channel input',
            description: '5.1->2 downmix producing 2-channel output'
          },
          (task, should) => {
            // Scale tolerance by maximum amplitude expected in down-mix
            // output.
            let threshold = (1.0 + Math.sqrt(0.5) * 2) * absoluteThreshold;

            downMixTest({numberOfInputs: 6, prefix: '5.1',
                         absoluteThreshold: threshold}, should)
                .then(() => task.done());
          });

      audit.define(
          {
            label: '3-channel input, explicit',
            description: '3->2 explicit downmix producing 2-channel output'
          },
          (task, should) => {
            downMixTest(
                {
                  channelCountMode: 'explicit',
                  numberOfInputs: 3,
                  prefix: '3 chan downmix explicit'
                },
                should)
                .then(() => task.done());
          });

      audit.define(
          {
            label: '4-channel input, explicit',
            description: '4->2 explicit downmix producing 2-channel output'
          },
          (task, should) => {
            downMixTest(
                {
                  channelCountMode: 'explicit',
                  numberOfInputs: 4,
                  prefix: '4 chan downmix explicit'
                },
                should)
                .then(() => task.done());
          });

      audit.define(
          {
            label: '5.1-channel input, explicit',
            description: '5.1->2 explicit downmix producing 2-channel output'
          },
          (task, should) => {
            // Scale tolerance by maximum amplitude expected in down-mix
            // output.
            let threshold = (1.0 + Math.sqrt(0.5) * 2) * absoluteThreshold;

            downMixTest(
                {
                  channelCountMode: 'explicit',
                  numberOfInputs: 6,
                  prefix: '5.1 chan downmix explicit',
                  absoluteThreshold: threshold
                },
                should)
                .then(() => task.done());
          });

      audit.define(
          {
            label: 'mono-upmix-explicit',
            description: '1->2 upmix, count mode explicit'
          },
          (task, should) => {
            upMixTest(should, {channelCountMode: 'explicit'})
                .then(buffer => {
                  let length = buffer.length;
                  let input = buffer.getChannelData(0);
                  let out0 = buffer.getChannelData(1);
                  let out1 = buffer.getChannelData(2);

                  // The convolver is basically a one-sample delay.  Verify that
                  // that each channel is delayed by one sample.
                  should(out0.slice(1), '1->2 explicit upmix: channel 0')
                      .beCloseToArray(
                          input.slice(0, length - 1),
                          {absoluteThreshold: absoluteThreshold});
                  should(out1.slice(1), '1->2 explicit upmix: channel 1')
                      .beCloseToArray(
                          input.slice(0, length - 1),
                          {absoluteThreshold: absoluteThreshold});
                })
                .then(() => task.done());
          });

      audit.define(
          {
            label: 'mono-upmix-clamped-max',
            description: '1->2 upmix, count mode clamped-max'
          },
          (task, should) => {
            upMixTest(should, {channelCountMode: 'clamped-max'})
                .then(buffer => {
                  let length = buffer.length;
                  let input = buffer.getChannelData(0);
                  let out0 = buffer.getChannelData(1);
                  let out1 = buffer.getChannelData(2);

                  // The convolver is basically a one-sample delay.  With a mono
                  // input, the count set to 2, and a mode of 'clamped-max', the
                  // output should be mono
                  should(out0.slice(1), '1->2 clamped-max upmix: channel 0')
                      .beCloseToArray(
                          input.slice(0, length - 1),
                          {absoluteThreshold: absoluteThreshold});
                  should(out1, '1->2 clamped-max upmix: channel 1')
                      .beConstantValueOf(0);
                })
                .then(() => task.done());
          });

      function downMixTest(options, should) {
        // Create an 4-channel offline context.  The first two channels are for
        // the stereo output of the convolver and the next two channels are for
        // the reference stereo signal.
        let context = new OfflineAudioContext(4, renderFrames, sampleRate);
        context.destination.channelInterpretation = 'discrete';

        // Create oscillators for use as the input.  The type and frequency is
        // arbitrary except that oscillators must be different.
        let src = new Array(options.numberOfInputs);
        for (let k = 0; k < src.length; ++k) {
          src[k] = new OscillatorNode(
              context, {type: 'square', frequency: 440 + 220 * k});
        }

        // Merger to combine the oscillators into one output stream.
        let srcMerger =
            new ChannelMergerNode(context, {numberOfInputs: src.length});

        for (let k = 0; k < src.length; ++k) {
          src[k].connect(srcMerger, 0, k);
        }

        // Convolver under test.
        let conv = new ConvolverNode(context, {
          disableNormalization: true,
          buffer: response,
          channelCountMode: options.channelCountMode
        });
        srcMerger.connect(conv);

        // Splitter to get individual channels of the convolver output so we can
        // feed them (eventually) to the context in the right set of channels.
        let splitter = new ChannelSplitterNode(context, {numberOfOutputs: 2});
        conv.connect(splitter);

        // Reference graph consists of a delay node to simulate the response of
        // the convolver.  (The convolver response is designed this way.)
        let delay = new DelayNode(context, {delayTime: 1 / context.sampleRate});

        // Gain node to mix the sources to stereo in the desired way.  (Could be
        // done in the delay node, but let's keep the mixing separated from the
        // functionality.)
        let gainMixer = new GainNode(
            context, {channelCount: 2, channelCountMode: 'explicit'});
        srcMerger.connect(gainMixer);

        // Splitter to extract the channels of the reference signal.
        let refSplitter =
            new ChannelSplitterNode(context, {numberOfOutputs: 2});
        gainMixer.connect(delay).connect(refSplitter);

        // Final merger to bring back the individual channels from the convolver
        // and the reference in the right order for the destination.
        let finalMerger = new ChannelMergerNode(
            context, {numberOfInputs: context.destination.channelCount});

        // First two channels are for the convolver output, and the next two are
        // for the reference.
        splitter.connect(finalMerger, 0, 0);
        splitter.connect(finalMerger, 1, 1);
        refSplitter.connect(finalMerger, 0, 2);
        refSplitter.connect(finalMerger, 1, 3);

        finalMerger.connect(context.destination);

        // Start the sources at last.
        for (let k = 0; k < src.length; ++k) {
          src[k].start();
        }

        return context.startRendering().then(audioBuffer => {
          // Extract the various channels out
          let actual0 = audioBuffer.getChannelData(0);
          let actual1 = audioBuffer.getChannelData(1);
          let expected0 = audioBuffer.getChannelData(2);
          let expected1 = audioBuffer.getChannelData(3);

          let threshold = options.absoluteThreshold ?
              options.absoluteThreshold : absoluteThreshold;

          // Verify that each output channel of the convolver matches
          // the delayed signal from the reference
          should(actual0, options.prefix + ': Channel 0')
              .beCloseToArray(expected0, {absoluteThreshold: threshold});
          should(actual1, options.prefix + ': Channel 1')
              .beCloseToArray(expected1, {absoluteThreshold: threshold});
        });
      }

      function upMixTest(should, options) {
        // Offline context with 3 channels:  0 = source
        // 1 = convolver output, left, 2 = convolver output, right.  Context
        // destination must be discrete so that channels don't get mixed in
        // unexpected ways.
        let context = new OfflineAudioContext(3, renderFrames, sampleRate);
        context.destination.channelInterpretation = 'discrete';

        let merger = new ChannelMergerNode(
            context, {numberOfInputs: context.destination.maxChannelCount});
        merger.connect(context.destination);

        let src = new OscillatorNode(context);

        // Mono response for convolver.  Just a simple 1-frame delay.
        let response =
            new AudioBuffer({length: 2, sampleRate: context.sampleRate});
        response.getChannelData(0)[1] = 1;

        // Set mode to explicit and count to 2 so we manually force the
        // convolver to produce stereo output.  Without this, it would be
        // mono input with mono response, which produces a mono output.
        let conv;

        should(
            () => {conv = new ConvolverNode(context, {
                     buffer: response,
                     disableNormalization: true,
                     channelCount: 2,
                     channelCountMode: options.channelCountMode
                   })},
            `new ConvolverNode({channelCountMode: '${
                options.channelCountMode}'})`)
            .notThrow();

        // Split output of convolver into individual channels.
        let convSplit = new ChannelSplitterNode(context, {numberOfOutputs: 2});

        src.connect(conv);
        conv.connect(convSplit);

        // Connect signals to destination in the desired way.
        src.connect(merger, 0, 0);
        convSplit.connect(merger, 0, 1);
        convSplit.connect(merger, 1, 2);

        src.start();

        return context.startRendering();
      }

      audit.run();
    </script>
  </body>
</html>
